<?php

namespace yii\base;

use Yii;

/**
 * BaseObject 是实现了 '属性' 的基类
 *
 * 属性由getter方法（例如`getLabel`）和/或setter方法（例如`setLabel`）定义。 例如，
 *
 * 属性名称不区分大小写。
 *
 * 可以像访问对象的成员变量一样访问属性。 读取或写入属性将会调用相应的getter或setter方法。 例如，
 *
 * 只读属性只有getter方法二没有setter方法，如果修改只读属性将会抛出异常。
 *
 * 可以调用[[hasProperty()]]，[[canGetProperty()]]和/或[[canSetProperty()]]来检查属性是否存在。
 *
 * 除了属性功能外，BaseObject 还引入了一个重要的对象初始化生命周期。
 * 创建 BaseObject 或其派生类的新实例将依次涉及以下生命周期：
 *
 * 1. 调用对象构造器 constructor
 * 2. 根据给定的配置初始化对象属性;
 * 3. 调用 `init()` 方法
 *
 * 在上面，步骤2和3都发生在类构造函数的末尾。 建议在 `init()` 方法中进行初始化操作，
 * 因为此时，对象配置已经完成。
 *
 * 为了确保上述生命周期，如果 BaseObject 的子类需要重载构造函数，需要调用父类的构造器方法。
 *
 * ```php
 * public function __construct($param1, $param2, ..., $config = [])
 * {
 *     ...
 *     parent::__construct($config);
 * }
 * ```
 * 也就是说，`$ config`参数（默认为`[]`）应声明为构造函数的最后一个参数，并且应在构造函数的末尾调用父实现。
 *
 */
class BaseObject implements Configurable
{
    /**
     * 返回类的完整名称
     * @return string 类的完整名称
     * @deprecated yii > 2.0.14 PHP >= 5.5 使用 ::class 代替
     */
    public static function className()
    {
        return get_called_class();
    }

    /**
     * 构造器
     *
     * 默认的构造函数做了两件事情
     *
     * - 通过配置数组 `$config` 初始化对象
     * - 调用 [[init()]] 方法
     *
     * If this method is overridden in a child class, it is recommended that
     * 如果子类重写该方法，推荐
     * - `$config` 数组作为方法的最后一个参数.
     * - 在结尾处调用弗雷德 `init()` 方法
     *
     * @param array $config 键值用来初始化对象的对应属性
     */
    public function __construct($config = [])
    {
        if (!empty($config)) {
            Yii::configure($this, $config);
        }
        $this->init();
    }

    /**
     * 初始化对象
     * 该方法在对象属性初始化完成之后调用
     */
    public function init()
    {
    }

    /**
     * 返回对象的属性值
     *
     * 不要直接调用该方法，作为改魔术方法将会在获取对象属性时触发
     * @param string $name 属性名
     * @return mixed 属性值
     * @throws UnknownPropertyException 未定义属性异常
     * @throws InvalidCallException 只写属性异常
     * @see __set()
     */
    public function __get($name)
    {
        $getter = 'get' . $name;
        if (method_exists($this, $getter)) {
            return $this->$getter();
        } elseif (method_exists($this, 'set' . $name)) {
            throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name);
        }

        throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name);
    }

    /**
     * 设置对象属性值
     *
     * 不要直接调用该方法，作为改魔术方法将会在为对象属相复制时触发
     * @param string $name 属性名或事件名
     * @param mixed $value 属性值
     * @throws UnknownPropertyException 未定义属性异常
     * @throws InvalidCallException 只写属性异常
     * @see __get()
     */
    public function __set($name, $value)
    {
        $setter = 'set' . $name;
        if (method_exists($this, $setter)) {
            $this->$setter($value);
        } elseif (method_exists($this, 'get' . $name)) {
            throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name);
        } else {
            throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name);
        }
    }

    /**
     * Checks if a property is set, i.e. defined and not null.
     *
     * Do not call this method directly as it is a PHP magic method that
     * will be implicitly called when executing `isset($object->property)`.
     *
     * Note that if the property is not defined, false will be returned.
     * @param string $name the property name or the event name
     * @return bool whether the named property is set (not null).
     * @see http://php.net/manual/en/function.isset.php
     */
    public function __isset($name)
    {
        $getter = 'get' . $name;
        if (method_exists($this, $getter)) {
            return $this->$getter() !== null;
        }

        return false;
    }

    /**
     * Sets an object property to null.
     *
     * Do not call this method directly as it is a PHP magic method that
     * will be implicitly called when executing `unset($object->property)`.
     *
     * Note that if the property is not defined, this method will do nothing.
     * If the property is read-only, it will throw an exception.
     * @param string $name the property name
     * @throws InvalidCallException if the property is read only.
     * @see http://php.net/manual/en/function.unset.php
     */
    public function __unset($name)
    {
        $setter = 'set' . $name;
        if (method_exists($this, $setter)) {
            $this->$setter(null);
        } elseif (method_exists($this, 'get' . $name)) {
            throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '::' . $name);
        }
    }

    /**
     * Calls the named method which is not a class method.
     *
     * Do not call this method directly as it is a PHP magic method that
     * will be implicitly called when an unknown method is being invoked.
     * @param string $name the method name
     * @param array $params method parameters
     * @throws UnknownMethodException when calling unknown method
     * @return mixed the method return value
     */
    public function __call($name, $params)
    {
        throw new UnknownMethodException('Calling unknown method: ' . get_class($this) . "::$name()");
    }

    /**
     * Returns a value indicating whether a property is defined.
     *
     * A property is defined if:
     *
     * - the class has a getter or setter method associated with the specified name
     *   (in this case, property name is case-insensitive);
     * - the class has a member variable with the specified name (when `$checkVars` is true);
     *
     * @param string $name the property name
     * @param bool $checkVars whether to treat member variables as properties
     * @return bool whether the property is defined
     * @see canGetProperty()
     * @see canSetProperty()
     */
    public function hasProperty($name, $checkVars = true)
    {
        return $this->canGetProperty($name, $checkVars) || $this->canSetProperty($name, false);
    }

    /**
     * Returns a value indicating whether a property can be read.
     *
     * A property is readable if:
     *
     * - the class has a getter method associated with the specified name
     *   (in this case, property name is case-insensitive);
     * - the class has a member variable with the specified name (when `$checkVars` is true);
     *
     * @param string $name the property name
     * @param bool $checkVars whether to treat member variables as properties
     * @return bool whether the property can be read
     * @see canSetProperty()
     */
    public function canGetProperty($name, $checkVars = true)
    {
        return method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name);
    }

    /**
     * Returns a value indicating whether a property can be set.
     *
     * A property is writable if:
     *
     * - the class has a setter method associated with the specified name
     *   (in this case, property name is case-insensitive);
     * - the class has a member variable with the specified name (when `$checkVars` is true);
     *
     * @param string $name the property name
     * @param bool $checkVars whether to treat member variables as properties
     * @return bool whether the property can be written
     * @see canGetProperty()
     */
    public function canSetProperty($name, $checkVars = true)
    {
        return method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name);
    }

    /**
     * Returns a value indicating whether a method is defined.
     *
     * The default implementation is a call to php function `method_exists()`.
     * You may override this method when you implemented the php magic method `__call()`.
     * @param string $name the method name
     * @return bool whether the method is defined
     */
    public function hasMethod($name)
    {
        return method_exists($this, $name);
    }
}
